feat(core/menu): Implement w3c menu pattern for ix-menu#2573
Conversation
🦋 Changeset detectedLatest commit: b58fbcf The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for ix-storybook ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Code Review
This pull request introduces keyboard navigation and a roving tab index mechanism for the ix-menu component, updating ix-menu-item and ix-menu-category to support dynamic tab index configuration. A critical review comment points out that using closest('ix-menu') to identify direct menu children is too broad and will break focusability for nested items in slots like ix-menu-settings, suggesting a direct parent check instead.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughImplements menubar keyboard navigation and ARIA updates for ChangesMenubar accessibility implementation
🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…nto feat/4304-menu-a11y
Improved **ix-menu** accessibility by implementing the w3c menubar pattern, adding better keyboard navigation and screenreader support.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
packages/core/src/components.d.ts (1)
2595-2690:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd the required
@sincedocs for the new public API.
setTabIndexandmenuCategoryLabelare new externally visible API surface, but these generated typings don’t carry the version metadata this repo requires for new props/methods.As per coding guidelines, new public API additions must include JSDoc with a
@sincetag in the source declarations so the generated types stay consistent. ``Also applies to: 9107-9170, 11757-11771
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/components.d.ts` around lines 2595 - 2690, The generated typings are missing `@since` tags for newly added public API; add JSDoc `@since` entries to the source declarations for the methods/props named setTabIndex and menuCategoryLabel so the generator emits the version metadata. Locate the component/source declaration files that define the IxMenuCategory/IxMenuItem APIs (the functions/properties setTabIndex and menuCategoryLabel) and prepend appropriate /** `@since` x.y.z */ JSDoc to each symbol (method signature for setTabIndex and the menuCategoryLabel prop declaration) using the correct release version.Source: Coding guidelines
packages/core/src/components/menu-category/test/menu-category.ct.ts (1)
9-9:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMissing accessibility test using
makeAxeBuilder.Per coding guidelines, every
.ct.tsfile must include an'accessibility'test usingmakeAxeBuilderto verify thataccessibilityScanResults.violationsreturns an empty array. This test file has a'renders'test but is missing the required accessibility test.💡 Suggested accessibility test
import { makeAxeBuilder } from '`@utils/test`'; regressionTest('accessibility', async ({ mount, page }) => { await mount(` <ix-application> <ix-menu> <ix-menu-category label="Category label"> <ix-menu-item>Test Item 1</ix-menu-item> <ix-menu-item>Test Item 2</ix-menu-item> </ix-menu-category> </ix-menu> </ix-application> `); const element = page.locator('ix-menu-category'); await expect(element).toHaveClass(/hydrated/); const results = await makeAxeBuilder({ page }).analyze(); expect(results.violations).toHaveLength(0); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/components/menu-category/test/menu-category.ct.ts` at line 9, Add a new regressionTest named "accessibility" to menu-category.ct.ts that uses makeAxeBuilder to scan the mounted component; specifically, mount the same markup used by the existing "renders" test (an ix-application with ix-menu and ix-menu-category containing two ix-menu-item children), await the component to hydrate via page.locator('ix-menu-category'), run makeAxeBuilder({ page }).analyze(), and assert that results.violations has length 0 using expect; ensure you import makeAxeBuilder from '`@utils/test`' and keep the test signature consistent with the existing regressionTest(async ({ mount, page }) => { ... }).Source: Coding guidelines
packages/core/src/components/toast/tests/toast.ct.ts (2)
19-171:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd the required axe accessibility regression for this
.ct.tsfile.This file has a
'renders'test, but it still lacks the mandatory'accessibility'test usingmakeAxeBuilder()and an emptyaccessibilityScanResults.violationsassertion. That leaves the menu-role change in this path uncovered by the repo’s required a11y gate. As per coding guidelines, "Ensure accessibility is tested with component tests using axe, verifying thataccessibilityScanResults.violationsreturns an empty array", "Every test file should include an'accessibility'test usingmakeAxeBuilder", and "Every test file should include a'renders'test verifying basic hydration".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/components/toast/tests/toast.ct.ts` around lines 19 - 171, Add a new regressionTest named 'accessibility' that mounts the component (reuse the same mount pattern used in the 'renders' test), uses makeAxeBuilder(page) to run an axe scan (include the toast element/selector), stores the result in accessibilityScanResults, and assert accessibilityScanResults.violations is an empty array; reference the existing regressionTest helper and makeAxeBuilder, and place the test alongside the other tests in this file so the repo's a11y gate is satisfied.Source: Coding guidelines
71-72:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReplace the fixed sleeps with state-based waits.
These
waitForTimeout(...)calls make the toast tests timing-sensitive and violate the component-test rules. Please wait on observable state instead—e.g. toast/menu visibility, attribute changes, orexpect.poll()for the pause/resume API—so the test follows the component rather than the clock. As per coding guidelines, "Don't use arbitrary timeouts orwaitForTimeoutin tests" and "Useexpect.poll()when a value requires async resolution or retries, but prefer Playwright's built-in auto-retry assertions whenever possible".Also applies to: 107-108, 115-116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/components/toast/tests/toast.ct.ts` around lines 71 - 72, The tests currently use page.waitForTimeout(...) (e.g., the fixed 500ms sleeps) which makes them timing-sensitive; replace each sleep with state-based waits that observe the component: use Playwright's auto-retry assertions like await expect(menuLocator).toBeVisible() / toBeHidden() or await expect(toastLocator).toHaveAttribute(...) for attribute changes, and use expect.poll() for the pause/resume API checks instead of fixed timeouts; update the places calling page.waitForTimeout to wait on the toast/menu locators or the pause/resume observable state (referencing the existing page.waitForTimeout calls, the toast/menu locators, and the pause/resume checks) so tests wait on component state not the clock.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.changeset/fair-falcons-search.md:
- Line 5: Update the changeset description string that currently reads "Improved
**ix-menu** accessibility by implementing the w3c menubar pattern, adding better
keyboard navigation and screenreader suppport." — fix the typo "suppport" →
"support" and capitalize the acronym "w3c" → "W3C" so the sentence becomes
"Improved **ix-menu** accessibility by implementing the W3C menubar pattern,
adding better keyboard navigation and screenreader support." Reference the same
changeset entry text in .changeset/fair-falcons-search.md to make the edits.
- Around line 1-5: The changeset for ix-menu is marked as patch but the
component adds new public API (new `@Prop/`@Event/@Method and exported interfaces
under packages/core/src/components/menu/*), so update
.changeset/fair-falcons-search.md to change the release type from patch to minor
and clearly list the `@siemens/ix` package; only add a "Fixes `#4304`" footer if the
PR/commit is actually linked to issue `#4304` (confirm the PR/issue connection
before adding that line).
In `@packages/core/src/components/menu-category/menu-category.tsx`:
- Around line 34-38: The helper createSequentialId currently increments its
parameter (sequenceId++) and callers also pass a post-incremented variable,
causing a redundant mutation; change createSequentialId to not mutate its input
(return `${prefix}-${sequenceId}`) and leave call-sites such as the usage with
categorySequenceId in menu-category.tsx and similar in menu-item.tsx using
sequenceId++ intact so the increment only happens at the call site; update the
createSequentialId implementation (and any docs/tests) to reflect the
non-mutating behavior.
In `@packages/core/src/components/menu-item/menu-item.tsx`:
- Around line 130-134: The new public API surface includes setTabIndex() (the
async method decorated with `@Method`() that sets hostTabIndex) but lacks
JSDoc/@since; either make it intentionally public by adding a JSDoc block above
setTabIndex() that documents the method and includes a `@since` tag with the
correct version, description and intended usage, or if it is internal plumbing
for roving tabindex, remove the `@Method`() decorator (and keep the method private
or rename to _setTabIndex) so it does not appear on the Stencil public surface;
update references to setTabIndex accordingly.
- Around line 218-233: getAriaLabel can produce "undefined …" when neither label
nor menuCategoryLabel is set; update getAriaLabel to compute a safe baseLabel
(e.g., const baseLabel = this.label ?? this.menuCategoryLabel ??
this.hostElement.textContent?.trim()) and use that instead of this.label when
building the distinct-tooltip branch; if baseLabel is falsy, return just
this.tooltipText (or undefined if tooltipText is also falsy) so you never
concatenate "undefined" with the tooltipText. Ensure changes are made inside the
getAriaLabel method and reference inheritAriaAttributes, tooltipText, label,
menuCategoryLabel, and hostElement.textContent.
In `@packages/core/src/components/menu/menu.tsx`:
- Around line 639-666: Fix the spelling typo in the comment above
updateMenuItemPositionMetadata: change "sepparated" to "separated" to improve
readability; locate the comment that reads "// item positions and menu size has
to be set manually because slotted items and utility controls are sepparated
into two groups" and update it accordingly (function:
updateMenuItemPositionMetadata, variable: allMenuItems).
- Around line 753-756: The switch branch handling 'End' indexes the last element
with items[items.length - 1]; change it to use the modern .at(-1) accessor for
clarity and safety: update the call to this.updateRovingTabIndex(items,
items.at(-1) ? items.length - 1 : items.length - 1) (or keep the index logic but
use items.at(-1) for retrieving the element to focus) and replace
items[items.length - 1].focus() with const last = items.at(-1); if (last) {
this.updateRovingTabIndex(items, items.length - 1); last.focus(); } to avoid
manual length arithmetic and null access in the case handlers.
---
Outside diff comments:
In `@packages/core/src/components.d.ts`:
- Around line 2595-2690: The generated typings are missing `@since` tags for newly
added public API; add JSDoc `@since` entries to the source declarations for the
methods/props named setTabIndex and menuCategoryLabel so the generator emits the
version metadata. Locate the component/source declaration files that define the
IxMenuCategory/IxMenuItem APIs (the functions/properties setTabIndex and
menuCategoryLabel) and prepend appropriate /** `@since` x.y.z */ JSDoc to each
symbol (method signature for setTabIndex and the menuCategoryLabel prop
declaration) using the correct release version.
In `@packages/core/src/components/menu-category/test/menu-category.ct.ts`:
- Line 9: Add a new regressionTest named "accessibility" to menu-category.ct.ts
that uses makeAxeBuilder to scan the mounted component; specifically, mount the
same markup used by the existing "renders" test (an ix-application with ix-menu
and ix-menu-category containing two ix-menu-item children), await the component
to hydrate via page.locator('ix-menu-category'), run makeAxeBuilder({ page
}).analyze(), and assert that results.violations has length 0 using expect;
ensure you import makeAxeBuilder from '`@utils/test`' and keep the test signature
consistent with the existing regressionTest(async ({ mount, page }) => { ... }).
In `@packages/core/src/components/toast/tests/toast.ct.ts`:
- Around line 19-171: Add a new regressionTest named 'accessibility' that mounts
the component (reuse the same mount pattern used in the 'renders' test), uses
makeAxeBuilder(page) to run an axe scan (include the toast element/selector),
stores the result in accessibilityScanResults, and assert
accessibilityScanResults.violations is an empty array; reference the existing
regressionTest helper and makeAxeBuilder, and place the test alongside the other
tests in this file so the repo's a11y gate is satisfied.
- Around line 71-72: The tests currently use page.waitForTimeout(...) (e.g., the
fixed 500ms sleeps) which makes them timing-sensitive; replace each sleep with
state-based waits that observe the component: use Playwright's auto-retry
assertions like await expect(menuLocator).toBeVisible() / toBeHidden() or await
expect(toastLocator).toHaveAttribute(...) for attribute changes, and use
expect.poll() for the pause/resume API checks instead of fixed timeouts; update
the places calling page.waitForTimeout to wait on the toast/menu locators or the
pause/resume observable state (referencing the existing page.waitForTimeout
calls, the toast/menu locators, and the pause/resume checks) so tests wait on
component state not the clock.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d0b8b7e6-499a-484c-8a08-0f77eb6d5033
⛔ Files ignored due to path filters (4)
packages/react/src/components/components.server.tsis excluded by!packages/react/src/components/**packages/vue/src/components/ix-menu-item.tsis excluded by!packages/vue/src/components/**testing/visual-testing/__screenshots__/tests/menu/menu.e2e.ts/menu-link---should-render-menu-items-with-a-href-1-chromium---classic-dark-linux.pngis excluded by!**/*.pngtesting/visual-testing/__screenshots__/tests/menu/menu.e2e.ts/menu-link---should-render-menu-items-with-a-href-1-chromium---classic-light-linux.pngis excluded by!**/*.png
📒 Files selected for processing (10)
.changeset/fair-falcons-search.mdpackages/core/src/components.d.tspackages/core/src/components/menu-category/menu-category.tsxpackages/core/src/components/menu-category/test/menu-category.ct.tspackages/core/src/components/menu-item/menu-item.tsxpackages/core/src/components/menu/menu.scsspackages/core/src/components/menu/menu.tsxpackages/core/src/components/toast/tests/toast.ct.tstesting/visual-testing/tests/menu-about-news/menu-about-news.e2e.tstesting/visual-testing/tests/menu/link/index.html
|
|
|
regarding open sonar issues: |
|



IX-4304
💡 What is the current behavior?
Currently ix-menu does not have proper keyboard-navigation according to w3c menubar pattern: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/
🆕 What is the new behavior?
Implements the w3c keyboard-navigation and roles for better screenreader support.
🏁 Checklist
A pull request can only be merged if all of these conditions are met (where applicable):
pnpm test)pnpm lint)pnpm build, changes pushed)👨💻 Help & support
Summary by CodeRabbit
ix-menuand enhanced menubar support with rovingtabIndex, improved Arrow/Home/End navigation, and utility controls grouping.setTabIndex()APIs for menu items/categories and improved category labeling support; added typings forix-playground.aria-expanded.menuitemroles and interactions.